package tcln.sors.service.core;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.dom4j.tree.AbstractEntity;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import tcln.commons.assertion.Assert;
import tcln.commons.logging.Logger;
import tcln.commons.reflection.ClassUtils;
import tcln.commons.text.MessageFormat;

/**
 * A simple factory implementation which looks up for object's service via Spring beans. The rule for lookup action is:
 * <ul>
 * <li>This factory searches for a Spring bean whose name is "<i>{object-simple-class}</i>Service". For example: for
 * object <code>ObjectOne</code>, there should be a bean '<i>objectOneService</i>' in current application context.
 * <li>In case when there is no entity-specific bean, the default bean specified in either property
 * {@link #defaultServiceBeanName} or {@link #defaultEntityServiceBeanName} will be used. This choice depends on input
 * object class. See {@link #getDefaultServiceBeanName(Class)} for details.
 * <li>By default, all service beans are cached inside this factory to minimize the memory effect. However, in some
 * cases we need to inverse the idea and service beans should NOT be singleton. Property {@link #serviceBeanCached},
 * when reset, enables this feature.
 * </ul>
 * <b>Remark</b>: To prevent the application context to create service beans on startup, it's advised that all service
 * beans including the default one are declared as 'prototype' in current application context. Caching of these beans
 * can be managed via {@link #serviceBeanCached} flag in this factory.
 * 
 * @author Truong Chau Lien Ngoc (lienngoc@gmail.com)
 */
public class ManagementServiceFactoryImpl implements ManagementServiceFactory, InitializingBean,
        ApplicationContextAware {
    private static final Logger LOGGER = Logger.getFor(ManagementServiceFactoryImpl.class);

    protected ApplicationContext applicationContext;

    /**
     * Default service bean name which is used to lookup for default service implementation. This bean name is
     * mandatory.
     */
    protected String defaultServiceBeanName;

    /**
     * Default service bean name <b>for entities</b> which is used to lookup for default service implementation. This
     * bean name is optional and would be set to {@link #defaultServiceBeanName} on startup if there is no custom bean
     * name.
     */
    protected String defaultEntityServiceBeanName;

    /**
     * A flag indicating if service beans should be cached inside this factory. If it's <code>false</code>, everytime a
     * {@link #getFor(Class)} method is invoked, a new implementation of {@link ManagementService} is created and
     * returned.
     */
    protected boolean serviceBeanCached = true;

    /**
     * Cached service map for lookup if needed <i>since services are usually singleton</i>.
     * <p>
     * This map composes of (objectClass, {@link ManagementService} implementation) pairs.
     */
    protected Map<Class<?>, ManagementService<?>> serviceMap = new HashMap<Class<?>, ManagementService<?>>();

    /**
     * Sets new value for {@link #defaultServiceBeanName}.
     * 
     * @param defaultServiceBeanName
     * @deprecated used by Spring only.
     */
    @Deprecated
    public void setDefaultServiceBeanName(final String defaultServiceBeanName) {
        this.defaultServiceBeanName = defaultServiceBeanName;
    }

    /**
     * Sets new value for {@link #defaultEntityServiceBeanName}.
     * 
     * @param defaultEntityServiceBeanName
     * @deprecated used by Spring only.
     */
    @Deprecated
    public void setDefaultEntityServiceBeanName(String defaultEntityServiceBeanName) {
        this.defaultEntityServiceBeanName = defaultEntityServiceBeanName;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public <E, S extends ManagementService<E>> S getFor(Class<E> objectClass, Class<S> targetServiceClass) {
        return (S) getFor(objectClass);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public <E> ManagementService<E> getFor(Class<E> objectClass) {
        ManagementService<?> service = null;
        if (this.serviceBeanCached) {
            service = serviceMap.get(objectClass);
            if (service == null) {
                service = lookupServiceBean(objectClass, ManagementService.class);
                if (service == null) {
                    serviceMap.put(objectClass, service);
                }
            }
        } else {
            service = lookupServiceBean(objectClass, ManagementService.class);
        }

        return (ManagementService<E>) service;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.STATE.isTrue(StringUtils.isNotBlank(defaultServiceBeanName), "'defaultServiceBeanName' is required");

        if (StringUtils.isBlank(defaultEntityServiceBeanName)) {
            defaultEntityServiceBeanName = defaultServiceBeanName;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setApplicationContext(final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    /**
     * Returns correct default service bean name according to given object class.
     * 
     * @param objectClass
     * @return Either {@link #defaultServiceBeanName} or {@link #defaultEntityServiceBeanName}.
     */
    protected String getDefaultServiceBeanName(Class<?> objectClass) {
        if (ClassUtils.isAssignable(AbstractEntity.class, objectClass)) {
            return defaultEntityServiceBeanName;
        } else {
            return defaultServiceBeanName;
        }
    }

    @SuppressWarnings("unchecked")
    private <S> S lookupServiceBean(final Class<?> objectClass, Class<S> targetServiceClass) {
        Object serviceBean = null;

        // firstly, lookup custom service bean: "{object-simple-class}Service"
        String serviceBeanName = StringUtils.uncapitalize(objectClass.getSimpleName() + "Service");
        serviceBean = lookupServiceBeanByName(serviceBeanName);

        // secondly, lookup default service bean
        if (serviceBean == null) {
            serviceBeanName = getDefaultServiceBeanName(objectClass);
            serviceBean = lookupServiceBeanByName(serviceBeanName);
        }

        // thirdly, check for target service class correspondences
        if (serviceBean != null && !targetServiceClass.isAssignableFrom(serviceBean.getClass())) {
            serviceBean = null;
        }

        // finally, if this bean is a default one (or a subclass of the default one) --> set persistence class.
        if (serviceBean != null && serviceBean instanceof EntityManagementService<?>) {
            ((EntityManagementService<?>) serviceBean).setPersistenceEntityClass(objectClass);
        }

        return (S) serviceBean;
    }

    private Object lookupServiceBeanByName(String serviceBeanName) {
        LOGGER.debug("Try to lookup service bean under name '" + serviceBeanName + "'");
        Object serviceBean = null;
        try {
            serviceBean = applicationContext.getBean(serviceBeanName);
        } catch (BeansException e) {
            LOGGER.debug(MessageFormat.TRADITIONAL.format("There is no custom service bean under name '{0}'",
                    serviceBeanName));
            serviceBean = null;
        }

        return serviceBean;
    }
}
